
package com.tiger.quicknews.wedget.flipview;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.Scroller;

import com.tiger.quicknews.R;
import com.tiger.quicknews.wedget.flipview.Recycler.Scrap;

public class FlipView extends FrameLayout {

    public interface OnFlipListener {
        public void onFlippedToPage(FlipView v, int position, long id);
    }

    public interface OnOverFlipListener {
        public void onOverFlip(FlipView v, OverFlipMode mode,
                boolean overFlippingPrevious, float overFlipDistance,
                float flipDistancePerPage);
    }

    /**
     * @author emilsjolander Class to hold a view and its corresponding info
     */
    static class Page {
        View v;
        int position;
        int viewType;
        boolean valid;
    }

    // this will be the postion when there is not data
    private static final int INVALID_PAGE_POSITION = -1;
    // "null" flip distance
    private static final int INVALID_FLIP_DISTANCE = -1;

    private static final int PEAK_ANIM_DURATION = 600;// in ms
    private static final int MAX_SINGLE_PAGE_FLIP_ANIM_DURATION = 300;// in ms

    // for normalizing width/height
    private static final int FLIP_DISTANCE_PER_PAGE = 180;
    private static final int MAX_SHADOW_ALPHA = 180;// out of 255
    private static final int MAX_SHADE_ALPHA = 130;// out of 255
    private static final int MAX_SHINE_ALPHA = 100;// out of 255

    // value for no pointer
    private static final int INVALID_POINTER = -1;

    // constant used by the attributes
    private static final int VERTICAL_FLIP = 0;

    // constant used by the attributes
    @SuppressWarnings("unused")
    private static final int HORIZONTAL_FLIP = 1;

    private final DataSetObserver dataSetObserver = new DataSetObserver() {

        @Override
        public void onChanged() {
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            dataSetInvalidated();
        }

    };

    private Scroller mScroller;
    private final Interpolator flipInterpolator = new DecelerateInterpolator();
    private ValueAnimator mPeakAnim;
    private final TimeInterpolator mPeakInterpolator = new AccelerateDecelerateInterpolator();

    private boolean mIsFlippingVertically = true;
    private boolean mIsFlipping;
    private boolean mIsUnableToFlip;
    private final boolean mIsFlippingEnabled = true;
    private boolean mLastTouchAllowed = true;
    private int mTouchSlop;
    private boolean mIsOverFlipping;

    // keep track of pointer
    private float mLastX = -1;
    private float mLastY = -1;
    private int mActivePointerId = INVALID_POINTER;

    // velocity stuff
    private VelocityTracker mVelocityTracker;
    private int mMinimumVelocity;
    private int mMaximumVelocity;

    // views get recycled after they have been pushed out of the active queue
    private Recycler mRecycler = new Recycler();

    private ListAdapter mAdapter;
    private int mPageCount = 0;
    private final Page mPreviousPage = new Page();
    private final Page mCurrentPage = new Page();
    private final Page mNextPage = new Page();
    private View mEmptyView;

    private OnFlipListener mOnFlipListener;
    private OnOverFlipListener mOnOverFlipListener;

    private float mFlipDistance = INVALID_FLIP_DISTANCE;
    private int mCurrentPageIndex = INVALID_PAGE_POSITION;
    private int mLastDispatchedPageEventIndex = 0;
    private long mCurrentPageId = 0;

    private OverFlipMode mOverFlipMode;
    private OverFlipper mOverFlipper;

    // clipping rects
    private final Rect mTopRect = new Rect();
    private final Rect mBottomRect = new Rect();
    private final Rect mRightRect = new Rect();
    private final Rect mLeftRect = new Rect();

    // used for transforming the canvas
    private final Camera mCamera = new Camera();
    private final Matrix mMatrix = new Matrix();

    // paints drawn above views when flipping
    private final Paint mShadowPaint = new Paint();
    private final Paint mShadePaint = new Paint();
    private final Paint mShinePaint = new Paint();

    public FlipView(Context context) {
        this(context, null);
    }

    public FlipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlipView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.FlipView);

        // 0 is vertical, 1 is horizontal
        mIsFlippingVertically = a.getInt(R.styleable.FlipView_orientation,
                VERTICAL_FLIP) == VERTICAL_FLIP;

        setOverFlipMode(OverFlipMode.values()[a.getInt(
                R.styleable.FlipView_overFlipMode, 0)]);

        a.recycle();

        init();
    }

    private void init() {
        final Context context = getContext();
        final ViewConfiguration configuration = ViewConfiguration.get(context);

        mScroller = new Scroller(context, flipInterpolator);
        mTouchSlop = configuration.getScaledPagingTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

        mShadowPaint.setColor(Color.BLACK);
        mShadowPaint.setStyle(Style.FILL);
        mShadePaint.setColor(Color.BLACK);
        mShadePaint.setStyle(Style.FILL);
        mShinePaint.setColor(Color.WHITE);
        mShinePaint.setStyle(Style.FILL);
    }

    private void dataSetChanged() {
        final int currentPage = mCurrentPageIndex;
        int newPosition = currentPage;

        // if the adapter has stable ids, try to keep the page currently on
        // stable.
        if (mAdapter.hasStableIds() && currentPage != INVALID_PAGE_POSITION) {
            newPosition = getNewPositionOfCurrentPage();
        } else if (currentPage == INVALID_PAGE_POSITION) {
            newPosition = 0;
        }

        // remove all the current views
        recycleActiveViews();
        mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
        mRecycler.invalidateScraps();

        mPageCount = mAdapter.getCount();

        // put the current page within the new adapter range
        newPosition = Math.min(mPageCount - 1,
                newPosition == INVALID_PAGE_POSITION ? 0 : newPosition);

        if (newPosition != INVALID_PAGE_POSITION) {
            // TODO pretty confusing
            // this will be correctly set in setFlipDistance method
            mCurrentPageIndex = INVALID_PAGE_POSITION;
            mFlipDistance = INVALID_FLIP_DISTANCE;
            flipTo(newPosition);
        } else {
            mFlipDistance = INVALID_FLIP_DISTANCE;
            mPageCount = 0;
            setFlipDistance(0);
        }

        updateEmptyStatus();
    }

    private int getNewPositionOfCurrentPage() {
        // check if id is on same position, this is because it will
        // often be that and this way you do not need to iterate the whole
        // dataset. If it is the same position, you are done.
        if (mCurrentPageId == mAdapter.getItemId(mCurrentPageIndex)) {
            return mCurrentPageIndex;
        }

        // iterate the dataset and look for the correct id. If it
        // exists, set that position as the current position.
        for (int i = 0; i < mAdapter.getCount(); i++) {
            if (mCurrentPageId == mAdapter.getItemId(i)) {
                return i;
            }
        }

        // Id no longer is dataset, keep current page
        return mCurrentPageIndex;
    }

    private void dataSetInvalidated() {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(dataSetObserver);
            mAdapter = null;
        }
        mRecycler = new Recycler();
        removeAllViews();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getDefaultSize(0, widthMeasureSpec);
        int height = getDefaultSize(0, heightMeasureSpec);

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    @Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getDefaultSize(0, widthMeasureSpec);
        int height = getDefaultSize(0, heightMeasureSpec);

        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                MeasureSpec.EXACTLY);
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }

    @Override
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChildren();

        mTopRect.top = 0;
        mTopRect.left = 0;
        mTopRect.right = getWidth();
        mTopRect.bottom = getHeight() / 2;

        mBottomRect.top = getHeight() / 2;
        mBottomRect.left = 0;
        mBottomRect.right = getWidth();
        mBottomRect.bottom = getHeight();

        mLeftRect.top = 0;
        mLeftRect.left = 0;
        mLeftRect.right = getWidth() / 2;
        mLeftRect.bottom = getHeight();

        mRightRect.top = 0;
        mRightRect.left = getWidth() / 2;
        mRightRect.right = getWidth();
        mRightRect.bottom = getHeight();
    }

    private void layoutChildren() {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            layoutChild(child);
        }
    }

    private void layoutChild(View child) {
        child.layout(0, 0, getWidth(), getHeight());
    }

    private void setFlipDistance(float flipDistance) {

        if (mPageCount < 1) {
            mFlipDistance = 0;
            mCurrentPageIndex = INVALID_PAGE_POSITION;
            mCurrentPageId = -1;
            recycleActiveViews();
            return;
        }

        if (flipDistance == mFlipDistance) {
            return;
        }

        mFlipDistance = flipDistance;

        final int currentPageIndex = Math.round(mFlipDistance
                / FLIP_DISTANCE_PER_PAGE);

        if (mCurrentPageIndex != currentPageIndex) {
            mCurrentPageIndex = currentPageIndex;
            mCurrentPageId = mAdapter.getItemId(mCurrentPageIndex);

            // TODO be smarter about this. Dont remove a view that will be added
            // again on the next line.
            recycleActiveViews();

            // add the new active views
            if (mCurrentPageIndex > 0) {
                fillPageForIndex(mPreviousPage, mCurrentPageIndex - 1);
                addView(mPreviousPage.v);
            }
            if (mCurrentPageIndex >= 0 && mCurrentPageIndex < mPageCount) {
                fillPageForIndex(mCurrentPage, mCurrentPageIndex);
                addView(mCurrentPage.v);
            }
            if (mCurrentPageIndex < mPageCount - 1) {
                fillPageForIndex(mNextPage, mCurrentPageIndex + 1);
                addView(mNextPage.v);
            }
        }

        invalidate();
    }

    private void fillPageForIndex(Page p, int i) {
        p.position = i;
        p.viewType = mAdapter.getItemViewType(p.position);
        p.v = getView(p.position, p.viewType);
        p.valid = true;
    }

    private void recycleActiveViews() {
        // remove and recycle the currently active views
        if (mPreviousPage.valid) {
            removeView(mPreviousPage.v);
            mRecycler.addScrapView(mPreviousPage.v, mPreviousPage.position,
                    mPreviousPage.viewType);
            mPreviousPage.valid = false;
        }
        if (mCurrentPage.valid) {
            removeView(mCurrentPage.v);
            mRecycler.addScrapView(mCurrentPage.v, mCurrentPage.position,
                    mCurrentPage.viewType);
            mCurrentPage.valid = false;
        }
        if (mNextPage.valid) {
            removeView(mNextPage.v);
            mRecycler.addScrapView(mNextPage.v, mNextPage.position,
                    mNextPage.viewType);
            mNextPage.valid = false;
        }
    }

    private View getView(int index, int viewType) {
        // get the scrap from the recycler corresponding to the correct view
        // type
        Scrap scrap = mRecycler.getScrapView(index, viewType);

        // get a view from the adapter if a scrap was not found or it is
        // invalid.
        View v = null;
        if (scrap == null || !scrap.valid) {
            v = mAdapter.getView(index, scrap == null ? null : scrap.v, this);
        } else {
            v = scrap.v;
        }

        // return view
        return v;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (!mIsFlippingEnabled) {
            return false;
        }

        if (mPageCount < 1) {
            return false;
        }

        final int action = ev.getAction() & MotionEvent.ACTION_MASK;

        if (action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_UP) {
            mIsFlipping = false;
            mIsUnableToFlip = false;
            mActivePointerId = INVALID_POINTER;
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN) {
            if (mIsFlipping) {
                return true;
            } else if (mIsUnableToFlip) {
                return false;
            }
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    break;
                }

                final int pointerIndex = MotionEventCompat.findPointerIndex(ev,
                        activePointerId);
                if (pointerIndex == -1) {
                    mActivePointerId = INVALID_POINTER;
                    break;
                }

                final float x = MotionEventCompat.getX(ev, pointerIndex);
                final float dx = x - mLastX;
                final float xDiff = Math.abs(dx);
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float dy = y - mLastY;
                final float yDiff = Math.abs(dy);

                if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff)
                        || (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) {
                    mIsFlipping = true;
                    mLastX = x;
                    mLastY = y;
                } else if ((mIsFlippingVertically && xDiff > mTouchSlop)
                        || (!mIsFlippingVertically && yDiff > mTouchSlop)) {
                    mIsUnableToFlip = true;
                }
                break;

            case MotionEvent.ACTION_DOWN:
                mActivePointerId = ev.getAction()
                        & MotionEvent.ACTION_POINTER_INDEX_MASK;
                mLastX = MotionEventCompat.getX(ev, mActivePointerId);
                mLastY = MotionEventCompat.getY(ev, mActivePointerId);

                mIsFlipping = !mScroller.isFinished() | mPeakAnim != null;
                mIsUnableToFlip = false;
                mLastTouchAllowed = true;

                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        if (!mIsFlipping) {
            trackVelocity(ev);
        }

        return mIsFlipping;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (!mIsFlippingEnabled) {
            return false;
        }

        if (mPageCount < 1) {
            return false;
        }

        if (!mIsFlipping && !mLastTouchAllowed) {
            return false;
        }

        final int action = ev.getAction();

        if (action == MotionEvent.ACTION_UP
                || action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_OUTSIDE) {
            mLastTouchAllowed = false;
        } else {
            mLastTouchAllowed = true;
        }

        trackVelocity(ev);

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:

                // start flipping immediately if interrupting some sort of
                // animation
                if (endScroll() || endPeak()) {
                    mIsFlipping = true;
                }

                // Remember where the motion event started
                mLastX = ev.getX();
                mLastY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mIsFlipping) {
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev,
                            mActivePointerId);
                    if (pointerIndex == -1) {
                        mActivePointerId = INVALID_POINTER;
                        break;
                    }
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float xDiff = Math.abs(x - mLastX);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    final float yDiff = Math.abs(y - mLastY);
                    if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff)
                            || (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) {
                        mIsFlipping = true;
                        mLastX = x;
                        mLastY = y;
                    }
                }
                if (mIsFlipping) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = MotionEventCompat
                            .findPointerIndex(ev, mActivePointerId);
                    if (activePointerIndex == -1) {
                        mActivePointerId = INVALID_POINTER;
                        break;
                    }
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    final float deltaX = mLastX - x;
                    final float y = MotionEventCompat.getY(ev, activePointerIndex);
                    final float deltaY = mLastY - y;
                    mLastX = x;
                    mLastY = y;

                    float deltaFlipDistance = 0;
                    if (mIsFlippingVertically) {
                        deltaFlipDistance = deltaY;
                    } else {
                        deltaFlipDistance = deltaX;
                    }

                    deltaFlipDistance /= ((isFlippingVertically() ? getHeight()
                            : getWidth()) / FLIP_DISTANCE_PER_PAGE);
                    setFlipDistance(mFlipDistance + deltaFlipDistance);

                    final int minFlipDistance = 0;
                    final int maxFlipDistance = (mPageCount - 1)
                            * FLIP_DISTANCE_PER_PAGE;
                    final boolean isOverFlipping = mFlipDistance < minFlipDistance
                            || mFlipDistance > maxFlipDistance;
                    if (isOverFlipping) {
                        mIsOverFlipping = true;
                        setFlipDistance(mOverFlipper.calculate(mFlipDistance,
                                minFlipDistance, maxFlipDistance));
                        if (mOnOverFlipListener != null) {
                            float overFlip = mOverFlipper.getTotalOverFlip();
                            mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
                                    overFlip < 0, Math.abs(overFlip),
                                    FLIP_DISTANCE_PER_PAGE);
                        }
                    } else if (mIsOverFlipping) {
                        mIsOverFlipping = false;
                        if (mOnOverFlipListener != null) {
                            // TODO in the future should only notify flip
                            // distance 0
                            // on the correct edge (previous/next)
                            mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
                                    false, 0, FLIP_DISTANCE_PER_PAGE);
                            mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
                                    true, 0, FLIP_DISTANCE_PER_PAGE);
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsFlipping) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

                    int velocity = 0;
                    if (isFlippingVertically()) {
                        velocity = (int) VelocityTrackerCompat.getYVelocity(
                                velocityTracker, mActivePointerId);
                    } else {
                        velocity = (int) VelocityTrackerCompat.getXVelocity(
                                velocityTracker, mActivePointerId);
                    }
                    smoothFlipTo(getNextPage(velocity));

                    mActivePointerId = INVALID_POINTER;
                    endFlip();

                    mOverFlipper.overFlipEnded();
                }
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(ev);
                final float x = MotionEventCompat.getX(ev, index);
                final float y = MotionEventCompat.getY(ev, index);
                mLastX = x;
                mLastY = y;
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                break;
            }
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                final int index = MotionEventCompat.findPointerIndex(ev,
                        mActivePointerId);
                final float x = MotionEventCompat.getX(ev, index);
                final float y = MotionEventCompat.getY(ev, index);
                mLastX = x;
                mLastY = y;
                break;
        }
        if (mActivePointerId == INVALID_POINTER) {
            mLastTouchAllowed = false;
        }
        return true;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {

        if (mPageCount < 1) {
            return;
        }

        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
            setFlipDistance(mScroller.getCurrY());
        }

        if (mIsFlipping || !mScroller.isFinished() || mPeakAnim != null) {
            showAllPages();
            drawPreviousHalf(canvas);
            drawNextHalf(canvas);
            drawFlippingHalf(canvas);
        } else {
            endScroll();
            setDrawWithLayer(mCurrentPage.v, false);
            hideOtherPages(mCurrentPage);
            drawChild(canvas, mCurrentPage.v, 0);

            // dispatch listener event now that we have "landed" on a page.
            // TODO not the prettiest to have this with the drawing logic,
            // should change.
            if (mLastDispatchedPageEventIndex != mCurrentPageIndex) {
                mLastDispatchedPageEventIndex = mCurrentPageIndex;
                postFlippedToPage(mCurrentPageIndex);
            }
        }

        // if overflip is GLOW mode and the edge effects needed drawing, make
        // sure to invalidate
        if (mOverFlipper.draw(canvas)) {
            // always invalidate whole screen as it is needed 99% of the time.
            // This is because of the shadows and shines put on the non-flipping
            // pages
            invalidate();
        }
    }

    private void hideOtherPages(Page p) {
        if (mPreviousPage != p && mPreviousPage.valid && mPreviousPage.v.getVisibility() != GONE) {
            mPreviousPage.v.setVisibility(GONE);
        }
        if (mCurrentPage != p && mCurrentPage.valid && mCurrentPage.v.getVisibility() != GONE) {
            mCurrentPage.v.setVisibility(GONE);
        }
        if (mNextPage != p && mNextPage.valid && mNextPage.v.getVisibility() != GONE) {
            mNextPage.v.setVisibility(GONE);
        }
        p.v.setVisibility(VISIBLE);
    }

    private void showAllPages() {
        if (mPreviousPage.valid && mPreviousPage.v.getVisibility() != VISIBLE) {
            mPreviousPage.v.setVisibility(VISIBLE);
        }
        if (mCurrentPage.valid && mCurrentPage.v.getVisibility() != VISIBLE) {
            mCurrentPage.v.setVisibility(VISIBLE);
        }
        if (mNextPage.valid && mNextPage.v.getVisibility() != VISIBLE) {
            mNextPage.v.setVisibility(VISIBLE);
        }
    }

    /**
     * draw top/left half
     * 
     * @param canvas
     */
    private void drawPreviousHalf(Canvas canvas) {
        canvas.save();
        canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect);

        final float degreesFlipped = getDegreesFlipped();
        final Page p = degreesFlipped > 90 ? mPreviousPage : mCurrentPage;

        // if the view does not exist, skip drawing it
        if (p.valid) {
            setDrawWithLayer(p.v, true);
            drawChild(canvas, p.v, 0);
        }

        drawPreviousShadow(canvas);
        canvas.restore();
    }

    /**
     * draw top/left half shadow
     * 
     * @param canvas
     */
    private void drawPreviousShadow(Canvas canvas) {
        final float degreesFlipped = getDegreesFlipped();
        if (degreesFlipped > 90) {
            final int alpha = (int) (((degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA);
            mShadowPaint.setAlpha(alpha);
            canvas.drawPaint(mShadowPaint);
        }
    }

    /**
     * draw bottom/right half
     * 
     * @param canvas
     */
    private void drawNextHalf(Canvas canvas) {
        canvas.save();
        canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect);

        final float degreesFlipped = getDegreesFlipped();
        final Page p = degreesFlipped > 90 ? mCurrentPage : mNextPage;

        // if the view does not exist, skip drawing it
        if (p.valid) {
            setDrawWithLayer(p.v, true);
            drawChild(canvas, p.v, 0);
        }

        drawNextShadow(canvas);
        canvas.restore();
    }

    /**
     * draw bottom/right half shadow
     * 
     * @param canvas
     */
    private void drawNextShadow(Canvas canvas) {
        final float degreesFlipped = getDegreesFlipped();
        if (degreesFlipped < 90) {
            final int alpha = (int) ((Math.abs(degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA);
            mShadowPaint.setAlpha(alpha);
            canvas.drawPaint(mShadowPaint);
        }
    }

    private void drawFlippingHalf(Canvas canvas) {
        canvas.save();
        mCamera.save();

        final float degreesFlipped = getDegreesFlipped();

        if (degreesFlipped > 90) {
            canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect);
            if (mIsFlippingVertically) {
                mCamera.rotateX(degreesFlipped - 180);
            } else {
                mCamera.rotateY(180 - degreesFlipped);
            }
        } else {
            canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect);
            if (mIsFlippingVertically) {
                mCamera.rotateX(degreesFlipped);
            } else {
                mCamera.rotateY(-degreesFlipped);
            }
        }

        mCamera.getMatrix(mMatrix);

        positionMatrix();
        canvas.concat(mMatrix);

        setDrawWithLayer(mCurrentPage.v, true);
        drawChild(canvas, mCurrentPage.v, 0);

        drawFlippingShadeShine(canvas);

        mCamera.restore();
        canvas.restore();
    }

    /**
     * will draw a shade if flipping on the previous(top/left) half and a shine
     * if flipping on the next(bottom/right) half
     * 
     * @param canvas
     */
    private void drawFlippingShadeShine(Canvas canvas) {
        final float degreesFlipped = getDegreesFlipped();
        if (degreesFlipped < 90) {
            final int alpha = (int) ((degreesFlipped / 90f) * MAX_SHINE_ALPHA);
            mShinePaint.setAlpha(alpha);
            canvas.drawRect(isFlippingVertically() ? mBottomRect : mRightRect,
                    mShinePaint);
        } else {
            final int alpha = (int) ((Math.abs(degreesFlipped - 180) / 90f) * MAX_SHADE_ALPHA);
            mShadePaint.setAlpha(alpha);
            canvas.drawRect(isFlippingVertically() ? mTopRect : mLeftRect,
                    mShadePaint);
        }
    }

    /**
     * Enable a hardware layer for the view.
     * 
     * @param v
     * @param drawWithLayer
     */
    private void setDrawWithLayer(View v, boolean drawWithLayer) {
        try {
            if (isHardwareAccelerated()) {
                if (v.getLayerType() != LAYER_TYPE_HARDWARE && drawWithLayer) {
                    v.setLayerType(LAYER_TYPE_HARDWARE, null);
                } else if (v.getLayerType() != LAYER_TYPE_NONE && !drawWithLayer) {
                    v.setLayerType(LAYER_TYPE_NONE, null);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void positionMatrix() {
        mMatrix.preScale(0.25f, 0.25f);
        mMatrix.postScale(4.0f, 4.0f);
        mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
        mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
    }

    private float getDegreesFlipped() {
        float localFlipDistance = mFlipDistance % FLIP_DISTANCE_PER_PAGE;

        // fix for negative modulo. always want a positive flip degree
        if (localFlipDistance < 0) {
            localFlipDistance += FLIP_DISTANCE_PER_PAGE;
        }

        return (localFlipDistance / FLIP_DISTANCE_PER_PAGE) * 180;
    }

    private void postFlippedToPage(final int page) {
        post(new Runnable() {

            @Override
            public void run() {
                if (mOnFlipListener != null) {
                    mOnFlipListener.onFlippedToPage(FlipView.this, page,
                            mAdapter.getItemId(page));
                }
            }
        });
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastX = MotionEventCompat.getX(ev, newPointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev,
                    newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    /**
     * @param deltaFlipDistance The distance to flip.
     * @return The duration for a flip, bigger deltaFlipDistance = longer
     *         duration. The increase if duration gets smaller for bigger values
     *         of deltaFlipDistance.
     */
    private int getFlipDuration(int deltaFlipDistance) {
        float distance = Math.abs(deltaFlipDistance);
        return (int) (MAX_SINGLE_PAGE_FLIP_ANIM_DURATION * Math.sqrt(distance
                / FLIP_DISTANCE_PER_PAGE));
    }

    /**
     * @param velocity
     * @return the page you should "land" on
     */
    private int getNextPage(int velocity) {
        int nextPage;
        if (velocity > mMinimumVelocity) {
            nextPage = getCurrentPageFloor();
        } else if (velocity < -mMinimumVelocity) {
            nextPage = getCurrentPageCeil();
        } else {
            nextPage = getCurrentPageRound();
        }
        return Math.min(Math.max(nextPage, 0), mPageCount - 1);
    }

    private int getCurrentPageRound() {
        return Math.round(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
    }

    private int getCurrentPageFloor() {
        return (int) Math.floor(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
    }

    private int getCurrentPageCeil() {
        return (int) Math.ceil(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
    }

    /**
     * @return true if ended a flip
     */
    private boolean endFlip() {
        final boolean wasflipping = mIsFlipping;
        mIsFlipping = false;
        mIsUnableToFlip = false;
        mLastTouchAllowed = false;

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        return wasflipping;
    }

    /**
     * @return true if ended a scroll
     */
    private boolean endScroll() {
        final boolean wasScrolling = !mScroller.isFinished();
        mScroller.abortAnimation();
        return wasScrolling;
    }

    /**
     * @return true if ended a peak
     */
    private boolean endPeak() {
        final boolean wasPeaking = mPeakAnim != null;
        if (mPeakAnim != null) {
            mPeakAnim.cancel();
            mPeakAnim = null;
        }
        return wasPeaking;
    }

    private void peak(boolean next, boolean once) {
        final float baseFlipDistance = mCurrentPageIndex
                * FLIP_DISTANCE_PER_PAGE;
        if (next) {
            mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance,
                    baseFlipDistance + FLIP_DISTANCE_PER_PAGE / 4);
        } else {
            mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance,
                    baseFlipDistance - FLIP_DISTANCE_PER_PAGE / 4);
        }
        mPeakAnim.setInterpolator(mPeakInterpolator);
        mPeakAnim.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                setFlipDistance((Float) animation.getAnimatedValue());
            }
        });
        mPeakAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                endPeak();
            }
        });
        mPeakAnim.setDuration(PEAK_ANIM_DURATION);
        mPeakAnim.setRepeatMode(ValueAnimator.REVERSE);
        mPeakAnim.setRepeatCount(once ? 1 : ValueAnimator.INFINITE);
        mPeakAnim.start();
    }

    private void trackVelocity(MotionEvent ev) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
    }

    private void updateEmptyStatus() {
        boolean empty = mAdapter == null || mPageCount == 0;

        if (empty) {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.VISIBLE);
                setVisibility(View.GONE);
            } else {
                setVisibility(View.VISIBLE);
            }

        } else {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.GONE);
            }
            setVisibility(View.VISIBLE);
        }
    }

    /* ---------- API ---------- */

    /**
     * @param adapter a regular ListAdapter, not all methods if the list adapter
     *            are used by the flipview
     */
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(dataSetObserver);
        }

        // remove all the current views
        removeAllViews();

        mAdapter = adapter;
        mPageCount = adapter == null ? 0 : mAdapter.getCount();

        if (adapter != null) {
            mAdapter.registerDataSetObserver(dataSetObserver);

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
            mRecycler.invalidateScraps();
        }

        // TODO pretty confusing
        // this will be correctly set in setFlipDistance method
        mCurrentPageIndex = INVALID_PAGE_POSITION;
        mFlipDistance = INVALID_FLIP_DISTANCE;
        setFlipDistance(0);

        updateEmptyStatus();
    }

    public ListAdapter getAdapter() {
        return mAdapter;
    }

    public int getPageCount() {
        return mPageCount;
    }

    public int getCurrentPage() {
        return mCurrentPageIndex;
    }

    public void flipTo(int page) {
        if (page < 0 || page > mPageCount - 1) {
            throw new IllegalArgumentException("That page does not exist");
        }
        endFlip();
        setFlipDistance(page * FLIP_DISTANCE_PER_PAGE);
    }

    public void flipBy(int delta) {
        flipTo(mCurrentPageIndex + delta);
    }

    public void smoothFlipTo(int page) {
        if (page < 0 || page > mPageCount - 1) {
            throw new IllegalArgumentException("That page does not exist");
        }
        final int start = (int) mFlipDistance;
        final int delta = page * FLIP_DISTANCE_PER_PAGE - start;

        endFlip();
        mScroller.startScroll(0, start, 0, delta, getFlipDuration(delta));
        invalidate();
    }

    public void smoothFlipBy(int delta) {
        smoothFlipTo(mCurrentPageIndex + delta);
    }

    /**
     * Hint that there is a next page will do nothing if there is no next page
     * 
     * @param once if true, only peak once. else peak until user interacts with
     *            view
     */
    public void peakNext(boolean once) {
        if (mCurrentPageIndex < mPageCount - 1) {
            peak(true, once);
        }
    }

    /**
     * Hint that there is a previous page will do nothing if there is no
     * previous page
     * 
     * @param once if true, only peak once. else peak until user interacts with
     *            view
     */
    public void peakPrevious(boolean once) {
        if (mCurrentPageIndex > 0) {
            peak(false, once);
        }
    }

    /**
     * @return true if the view is flipping vertically, can only be set via xml
     *         attribute "orientation"
     */
    public boolean isFlippingVertically() {
        return mIsFlippingVertically;
    }

    /**
     * The OnFlipListener will notify you when a page has been fully turned.
     * 
     * @param onFlipListener
     */
    public void setOnFlipListener(OnFlipListener onFlipListener) {
        mOnFlipListener = onFlipListener;
    }

    /**
     * The OnOverFlipListener will notify of over flipping. This is a great
     * listener to have when implementing pull-to-refresh
     * 
     * @param onOverFlipListener
     */
    public void setOnOverFlipListener(OnOverFlipListener onOverFlipListener) {
        this.mOnOverFlipListener = onOverFlipListener;
    }

    /**
     * @return the overflip mode of this flipview. Default is GLOW
     */
    public OverFlipMode getOverFlipMode() {
        return mOverFlipMode;
    }

    /**
     * Set the overflip mode of the flipview. GLOW is the standard seen in all
     * andriod lists. RUBBER_BAND is more like iOS lists which list you flip
     * past the first/last page but adding friction, like a rubber band.
     * 
     * @param overFlipMode
     */
    public void setOverFlipMode(OverFlipMode overFlipMode) {
        this.mOverFlipMode = overFlipMode;
        mOverFlipper = OverFlipperFactory.create(this, mOverFlipMode);
    }

    /**
     * @param emptyView The view to show when either no adapter is set or the
     *            adapter has no items. This should be a view already in the
     *            view hierarchy which the FlipView will set the visibility of.
     */
    public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
        updateEmptyStatus();
    }

}
